﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Services;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Reflection;
using System.Globalization;
using System.Data;
using ADONETDataServicesExtension.Providers;
using ADONETDataServicesExtension.Utils;
using System.Collections;
using System.Linq.Expressions;
using System.Data.Services.Common;
using System.Web;

namespace ADONETDataServicesExtension.BLLUtils
{
    public abstract class BaseContext<T> : IEFEntitiesEncapsulatedContext, IUpdatable where T : ObjectContext, new()
    {
        private const int NotAllowedExceptionCode = 400;

        private T _efContext;
        protected T EFContext
        {
            get
            {
                if (_efContext == null)
                    _efContext = new T();
                return _efContext;
            }
        }

        private string Login
        {
            get { return HttpContext.Current.User.Identity.Name; }
        }

        IQueryable IEFEntitiesEncapsulatedContext.EncapsulateIntoBLLEntityType(ObjectQuery efQuery, Type type)
        {
            return EncapsulateIntoBLLEntityType(efQuery, type, Login);
        }
        protected abstract IQueryable EncapsulateIntoBLLEntityType(ObjectQuery efQuery, Type type, string login);
        ObjectQuery IEFEntitiesEncapsulatedContext.GetEFQuery(string propertyName)
        {
            return GetEFQuery(propertyName);
        }
        protected abstract ObjectQuery GetEFQuery(string propertyName);

        void IUpdatable.AddReferenceToCollection(object objTargetResource, string objPropertyName, object objResourceToBeAdded)
        {
            var entityNeedRightsToWrite = objTargetResource as IEntityNeedRightsToWrite;
            if (!(entityNeedRightsToWrite == null || entityNeedRightsToWrite.CanChangeLinks(Login, objPropertyName)))
                throw new System.Data.Services.DataServiceException(NotAllowedExceptionCode, Strings.OperationNotAllowed);
            var targetResource = ((IEntity)objTargetResource).EFEntity;
            PropertyInfo property = objTargetResource.GetType().GetProperty(objPropertyName);
            EFEntityPropertyAttribute efPropertyAttribute;
            if ((efPropertyAttribute = property.Attribute<EFEntityPropertyAttribute>()) == null)
            {
                var collection = ReflectionHelper.GetProperty(objTargetResource.GetType(), objPropertyName, objTargetResource);
                if (collection is IList)
                    ((IList)collection).Add(objResourceToBeAdded);
                else
                    throw new NotImplementedException();
                return;
            }
            string propertyName = efPropertyAttribute.EFPropertyName ?? objPropertyName;
            IEntityWithRelationships entity = ((IEntity)objResourceToBeAdded).EFEntity;
            ObjectStateEntry objectStateEntry = EFContext.ObjectStateManager.GetObjectStateEntry(targetResource);
            try
            {
                IRelatedEnd end = (IRelatedEnd)targetResource.GetType().InvokeMember(propertyName, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance, null, targetResource, new object[0], CultureInfo.InvariantCulture);
                if ((objectStateEntry.State == EntityState.Added) || (objectStateEntry.State == EntityState.Unchanged))
                    end.Add(entity);
                else
                    end.Attach(entity);
            }
            catch (TargetInvocationException exception)
            {
                ErrorHandler.HandleTargetInvocationException(exception);
                throw;
            }
        }
        void IUpdatable.ClearChanges()
        {
            var efContext = _efContext;
            _efContext = new T();
            efContext.Dispose();
        }
        object IUpdatable.CreateResource(string containerName, string fullTypeName)
        {
            var entityType = GetType().Assembly.GetType(fullTypeName);
            if (entityType.IsAbstract)
                DataServiceException.CreateBadRequestError(Strings.CannotCreateInstancesOfAbstractType(entityType.FullName)).Throw();
            var entity = ReflectionHelper.CreateInstance(entityType);
            var entityNeedRightsToWrite = entity as IEntityNeedRightsToWrite;
            if (!(entityNeedRightsToWrite == null || entityNeedRightsToWrite.CanAdd(Login)))
                throw new System.Data.Services.DataServiceException(NotAllowedExceptionCode, Strings.OperationNotAllowed);
            string efEntitySetName = GetEFEntitySetName(entityType);
            if (efEntitySetName != null && typeof(IEntity).IsAssignableFrom(entityType))
                EFContext.AddObject(efEntitySetName, ((IEntity)(entity)).EFEntity);
            return entity;
        }
        private static string GetEFEntitySetName(Type entityType)
        {
            foreach (var efEntityTypeAttribute in entityType.Attributes<EFEntityTypeAttribute>())
                if (efEntityTypeAttribute.EntitySetName != null)
                    return efEntityTypeAttribute.EntitySetName;
            return null;
        }
        private static Type GetEFType(Type entityType, object efContext)
        {
            foreach (var efEntityTypeAttribute in entityType.Attributes<EFEntityTypeAttribute>())
                if (efEntityTypeAttribute.TypeName != null)
                    return efContext.GetType().Assembly.GetType(efEntityTypeAttribute.TypeName);
            return null;
        }
        void IUpdatable.DeleteResource(object targetResource)
        {
            var entityNeedRightsToWrite = targetResource as IEntityNeedRightsToWrite;
            if (!(entityNeedRightsToWrite == null || entityNeedRightsToWrite.CanDelete(Login)))
                throw new System.Data.Services.DataServiceException(NotAllowedExceptionCode, Strings.OperationNotAllowed);
            EFContext.DeleteObject(((IEntity)(targetResource)).EFEntity);
        }
        object IUpdatable.GetResource(IQueryable query, string fullTypeName)
        {
            var entityType = query.ElementType;
            var efType = GetEFType(entityType, EFContext);
            MethodCallExpression methodCallExpression;
            LambdaExpression lambdaExpression;
            UnaryExpression unaryExpression;
            ObjectQuery oq = null;
            IEnumerable q;
            if ((methodCallExpression = query.Expression as MethodCallExpression) != null && (methodCallExpression.Arguments.Skip(1).Any()) && ((unaryExpression = methodCallExpression.Arguments[1] as UnaryExpression) != null) && ((lambdaExpression = unaryExpression.Operand as LambdaExpression) != null))
                oq = (ObjectQuery)(typeof(Queryable).GetMethods().Where(m => m.Name == "Where").First().MakeGenericMethod(efType).Invoke(null, new object[] { typeof(Queryable).GetMethods().Where(m => m.Name == "OfType").First().MakeGenericMethod(efType).Invoke(null, new object[] { ReflectionHelper.GetProperty(EFContext.GetType(), GetEFEntitySetName(entityType), EFContext) }), ObjectExpressionToEFExpressionConverter.ConvertExpression(lambdaExpression, entityType, efType) }));
            if (oq != null)
            {
                oq.MergeOption = MergeOption.AppendOnly;
                q = oq;
            }
            else
                q = query;
            object obj2 = null;
            foreach (object obj3 in (IEnumerable)q)
            {
                if (obj2 != null)
                    throw new InvalidOperationException(Strings.SingleResourceExpected);
                obj2 = obj3;
            }
            if (obj2 == null)
                return null;
            //if (fullTypeName != null)
            //{
            //    ResourceType resourceType = (new IDataServiceProvider(this)).GetResourceType(obj2.GetType());
            //    if (resourceType.FullName != fullTypeName)
            //    {
            //        DataServiceException.CreateBadRequestError(Strings.TargetElementTypeOfTheUriSpecifiedDoesNotMatchWithTheExpectedType(resourceType.FullName, fullTypeName)).Throw();
            //    }
            //}
            if (oq == null)
                return obj2;
            return ReflectionHelper.CreateInstance(entityType, obj2);
        }
        object IUpdatable.GetValue(object targetResource, string propertyName)
        {
            return ReflectionHelper.GetProperty(targetResource.GetType(), propertyName, targetResource);
        }
        void IUpdatable.RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
        {
            var entityNeedRightsToWrite = targetResource as IEntityNeedRightsToWrite;
            if (!(entityNeedRightsToWrite == null || entityNeedRightsToWrite.CanChangeLinks(Login, propertyName)))
                throw new System.Data.Services.DataServiceException(NotAllowedExceptionCode, Strings.OperationNotAllowed);
            var efTargetResource = ((IEntity)targetResource).EFEntity;
            var efResourceToRemoved = ((IEntity)resourceToBeRemoved).EFEntity;
            var propertyAttribute = targetResource.GetType().GetProperty(propertyName).Attribute<EFEntityPropertyAttribute>();
            var efEntityCollection = ReflectionHelper.GetProperty<RelatedEnd>(efTargetResource.GetType(), propertyAttribute.EFPropertyName ?? propertyName, efTargetResource);
            if (!efEntityCollection.IsLoaded)
                efEntityCollection.Load();
            ReflectionHelper.InvokeMethod(efEntityCollection.GetType(), "Remove", efEntityCollection, efResourceToRemoved);
        }
        object IUpdatable.ResetResource(object resource)
        {
            var resourceType = resource.GetType();
            var value = ReflectionHelper.CreateInstance(resourceType);
            foreach (PropertyInfo prop in resourceType.Attributes<DataServiceKeyAttribute>().SelectMany(dska => dska.KeyNames).Select(propName => resourceType.GetProperty(propName)))
                prop.SetValue(value, prop.GetValue(resource, null), null);
            return value;
        }
        object IUpdatable.ResolveResource(object resource)
        {
            return resource;
        }
        void IUpdatable.SaveChanges()
        {
            EFContext.SaveChanges();
        }
        void IUpdatable.SetReference(object targetResource, string propertyName, object propertyValue)
        {
            var entityNeedRightsToWrite = targetResource as IEntityNeedRightsToWrite;
            if (!(entityNeedRightsToWrite == null || entityNeedRightsToWrite.CanChangeLinks(Login, propertyName)))
                throw new System.Data.Services.DataServiceException(NotAllowedExceptionCode, Strings.OperationNotAllowed);
            ReflectionHelper.SetProperty(targetResource.GetType(), propertyName, targetResource, propertyValue);
        }
        void IUpdatable.SetValue(object targetResource, string propertyName, object propertyValue)
        {
            if (ReflectionHelper.GetInstancePropertyInfo(targetResource.GetType(), propertyName).CanWrite)
            {
                var entityNeedRightsToWrite = targetResource as IEntityNeedRightsToWrite;
                if (! (entityNeedRightsToWrite == null || entityNeedRightsToWrite.CanChange(Login, propertyName)))
                    throw new System.Data.Services.DataServiceException(NotAllowedExceptionCode, Strings.OperationNotAllowed);
                ReflectionHelper.SetProperty(targetResource.GetType(), propertyName, targetResource, propertyValue);
            }
            else
                throw new InvalidOperationException();
        }
    }
}
